# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2012  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

require 'redmine/scm/adapters/abstract_adapter'

module Redmine
  module Scm
    module Adapters
      class PlasticAdapter < AbstractAdapter

        class PlasticBranch < Branch 
          attr_accessor :is_default
        end

        class Revision < Redmine::Scm::Adapters::Revision
        end
        
        # Plastic executable name
        CM_BIN = Redmine::Configuration['scm_plastic_command'] || "cm"


        class << self
          def client_command
            @@bin    ||= CM_BIN
          end

          def sq_bin
            @@sq_bin ||= shell_quote_command
          end

          def client_version
            @@client_version ||= (scm_command_version || [])
          end

          def client_available
            !client_version.empty?
          end	

          def scm_command_version
            scm_version = scm_version_from_command_line.dup
            if scm_version.respond_to?(:force_encoding)
              scm_version.force_encoding('ASCII-8BIT')
            end
            #scm_version.is_a?(Array) ? scm_version.join('.') : scm_version.to_s.strip
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
              m[2].scan(%r{\d+}).collect(&:to_i)
            end            
          end

          def scm_version_from_command_line
            shellout("#{sq_bin} version") { |io| io.read }.to_s
          end
        end

        
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
          super
          logger.debug { "plastic initialize url:#{url}, root_url:#{root_url}" }
          #@root_url = root_url.blank? ? 'localhost:8087' : root_url
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
        end

        def path_encoding
          @path_encoding
        end

        def branches
          return @branches if @branches
          @branches = []
          repo_server = root_url
          #find branch where attribute = 'DEFAULT' and attrvalue = 'TRUE' --format='{name}'
          default_cmd_args = %w|find branch|
          default_cmd_args << %Q|where attribute='DEFAULT_BRANCH' and attrvalue='TRUE' --format="{name}" --nototal|
          default_cmd_args << "on repository '#{repo_server}'"
          de_branch = "/main"
          scm_cmd(default_cmd_args) do |io|
            io.each_line do |line|
              if line.start_with?('/')
                de_branch = line.chomp.to_s
              end
            end
          end
          logger.debug { "<plastic> branches de_branch:[#{de_branch}] " }
          cmd_args = %w|find branch on repository| << "\'#{repo_server}\'"
          cmd_args << "--nototal"
          cmd_args << '--format="{name} {id} {changeset}"'
          logger.debug { "<plastic> branches cmd_args:#{cmd_args} " }
          index_name = 0
          index_id = 1
          index_changeset = 2          
          scm_cmd(cmd_args) do |io|
            io.each_line do |line|
              branch_changeset = line.split
              if !branch_changeset[index_name].nil? && branch_changeset[index_name].start_with?('/')
                bran = PlasticBranch.new(branch_changeset[index_name].strip)
                bran.revision =  branch_changeset[index_changeset].to_i
                bran.scmid    =  branch_changeset[index_changeset].to_i
                bran.is_default = ( branch_changeset[index_name] == de_branch )
                @branches << bran                
              end              
            end
          end
          @branches.sort!
        rescue ScmCommandAborted
          nil
        end

        def default_branch
          bras = self.branches
          return nil if bras.nil?
          default_bras = bras.select{|x| x.is_default == true}
          return default_bras.first.to_s if ! default_bras.empty?
          main_bras = bras.select{|x| x.to_s == '/main'}
          main_bras.empty? ? bras.first.to_s : '/main' 
        end

        def tags
          return @tags if @tags
          #repo_name = url
          repo_server = root_url
          cmd_args = %w|find label on repository| << "\'#{repo_server}\'"
          cmd_args << '--nototal'
          cmd_args << '--format="{name}"'
          logger.debug { "branches cmd_args:#{cmd_args} " }
          scm_cmd(cmd_args) do |io|
            @tags = io.readlines.sort!.map{|t| t.strip}
          end
        rescue ScmCommandAborted
          nil
        end

        def annotate(path, identifier=nil)
          cmd_args = %w|annotate|
          cmd_args << path
          blame =  Annotate.new
          scm_cmd(cmd_args) { |io| io.binmode; content = io.read }
          blame
        rescue ScmCommandAborted
          nil          
        end

        # Returns the entry identified by path and revision identifier
        # or nil if entry doesn't exist in the repository
        #
        # add options = {:report_last_commit => false} when call entries
        def entry(path=nil, identifier=nil)
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
          search_path = parts[0..-2].join('/')
          search_name = parts[-1]
          if search_path.blank? && search_name.blank?
            # Root entry
            Entry.new(:path => '', :kind => 'dir')
          else
            # Search for the entry in the parent directory
            es = entries(search_path, identifier,
                         options = {:report_last_commit => false})
            es ? es.detect {|e| e.name == search_name} : nil
          end
        end
        
        # Returns an Entries collection
        # or nil if the given path doesn't exist in the repository
        #
        # default format string is: (cm ls)
        # "{<size>,10} {<date>:dd/MM/yyyy} {<date>:HH:mm}
        #  {<type>,-6} {<location>,-12} {<co>,-5} {name}
        #  {symlinktarget}"
        def entries(path=nil, identifier=nil, options={})
          logger.debug  { "<plastic> entries: path:#{path}, identifier:#{identifier}" }
          logger.debug  { "<plastic> entries: options:#{options}" }
          path ||= ''
          p = scm_iconv(@path_encoding, 'UTF-8', path)          
          repo_server = root_url
          branch_name = identifier.nil? ? default_branch : identifier
          cmd_args = %w|ls|
          cmd_args << "/#{p}"
          cmd_args << "--selector=\"rep '#{repo_server}' path '/' branch '#{branch_name}'\""
          cmd_args << '--format="{<size>,10}@{<type>,-6}@{<location>,-12}@{name}"'
          entries = Entries.new
          scm_cmd(cmd_args) do |io|
            io.each_line do |line|
              e = line.chomp.to_s.split('@')
              logger.debug  "entries: length:#{e.length}, #{e}"
              # 4 depends on format
              if e.length == 4
                size = e[0]
                type = e[1].strip
                lastrev = e[2].split('#')[1].strip
                name = e[3]
                if name.respond_to?(:force_encoding)
                  name.force_encoding(@path_encoding)
                end
                full_path = p.empty? ? name : "#{p}/#{name}"
                n      = scm_iconv('UTF-8', @path_encoding, name)
                kind = (type == "dir") ? 'dir' : (type == "txt") ? 'text' : 'file'
                next if (type == "dir") and (name == ".")
                entries << Entry.new({:name => n,
                  :path => full_path,
                  :kind => kind,
                  :size => (type == "dir") ? nil : size,
                  :lastrev => Revision.new({:identifier => lastrev })
                  }) unless entries.detect {|entry| entry.name == name}
              end
            end
          end
          entries.sort_by_name
        rescue ScmCommandAborted
          nil
        end

        #
        #
        #
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          logger.debug {"<plastic> revisions path=#{path}, from=#{identifier_from}, to=#{identifier_to}, options=#{options}"}
          revs = Revisions.new
          # setup cmd_args
          path_to_workspace = url
          repository_server = root_url
          brs ||= []
          if identifier_from || identifier_to
            each_br = {}
            if identifier_to
              each_br[:name] = identifier_to
            end
            if identifier_from
              each_br[:rev_from] = identifier_from
            end
            brs << each_br
          else
            scm_brs = branches
            scm_brs.each_index do |x|
            	each_br = { :name => scm_brs[x] }
            	unless options[:includes].blank?
              	each_br[:rev_to] = options[:includes][x]
              	
            	end
            	unless options[:excludes].blank?
              	each_br[:rev_from] = options[:excludes][x]
            	end
            	logger.debug { "<plastic> revisions ... each_br:#{each_br}"}
            	brs << each_br
            end
          end
          logger.debug { "<plastic> revisions ... brs:#{brs}"}          

          brs.each do |br|
            logger.debug { "<plastic> revisions .... br:#{br}" }
	          cmd_args =%w|find changeset where|
	          cmd_args << "branch=\'#{br[:name]}\'"	          
	          cmd_args << "and \"changesetid <= #{br[:rev_to]}\"" unless br[:rev_to].nil?
	          cmd_args << "and \"changesetid > #{br[:rev_from]}\"" unless br[:rev_from].nil?
	          cmd_args << 'on repository' << "\'#{repository_server}\'"
	          cmd_args << '--xml'
	          logger.debug { "<plastic> revisions cmd_args:#{cmd_args}" }
	          scm_cmd(cmd_args) do |io|
	            output = io.read
	            if output.respond_to?(:force_encoding)
	              output.force_encoding('UTF-8')
	            end
	            begin
	              files = []
	              doc = parse_xml(output)
	              # logger.debug { "doc:#{doc}" }
	              each_xml_element(doc['PLASTICQUERY'], 'CHANGESET') do |changeset|
	                changeset_name = changeset['CHANGESETID']['__content__']
	                # single log for each changeset
	                log_cmd_args = %w|log|
	                log_cmd_args << "cs:#{changeset_name}"
	                scm_cmd(log_cmd_args) do |log_io|
	                  log_io.each_line do |log_line|
	                    if m = log_line.match(%r{^\s([ACMD])\s(.+)\s+.?(\d+)$})
	                      logger.debug { "<plastic> revisions log_line: matched#{m[0]}" }
	                      logger.debug { "<plastic> revisions log_line: m[1]:#{m[1]}" }
	                      logger.debug { "<plastic> revisions log_line: m[2]:#{m[2]}" }
	                      logger.debug { "<plastic> revisions log_line: m[3]:#{m[3]}" }
				                fileaction = m[1]
	      			          filepath = m[2]
	                 	    p = scm_iconv('UTF-8', @path_encoding, filepath)                      
	                      files << {:action => fileaction, :path => p}
	                    end
	                  end
	                end
	                parents = [ changeset['PARENT']['__content__'] ]
	                attributes={
	                  :identifier => changeset['CHANGESETID']['__content__'],
	                  :scmid => changeset['ID']['__content__'],
	                  :name => changeset_name,
	                  :author => changeset['OWNER']['__content__'],
	                  :time => Time.parse(changeset['DATE']['__content__']),
	                  :message => changeset['COMMENT']['__content__'],
	                  :paths => files,
	                  :branch => changeset['BRANCH']['__content__'],
	                  :parents => parents
	                }
	                # logger.debug { "attribues: => #{attributes}" }
	                # :paths is not yet initialized
	                revision = Revision.new(attributes)
	                # logger.debug { "revision => #{revision}" }
	                if block_given?
		                yield revision
	    	          else
	        	        revs << revision
	                end
	              end
	            rescue
	            end
	          end
	        end
          revs
        rescue ScmCommandAborted => e
          err_msg = "plastic revisions error: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            revs
          end
        end        

        def lastrev(path, rev)
          return nil if path.brank?
          cmd_args =%w|ls|
          cmd_args << path
          lines = []
          scm_cmd(cmd_args) { |io| lines = io.readlines }
          begin
              id = lines[0].split[1]
              author = lines[1].match('Author:\s+(.*)$')[1]
              time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])

              Revision.new({
                :identifier => id,
                :scmid      => id,
                :author     => author,
                :time       => time,
                :message    => nil,
                :paths      => nil
                })
          rescue NoMethodError => e
              logger.error("The revision '#{path}' has a wrong format")
              return nil
          end
        rescue ScmCommandAborted
          nil
        end

        # 
        def diff(path, rev, rev_to=nil)
          logger.debug {"plastic diff path:#{path} rev:#{rev} rev_to:#{rev_to}" }
          path ||= ''
          cmd_args = []
          if rev_to
            cmd_args << "diff" << rev_to << rev
          else
            cmd_args << "diff" << rev
          end
          diff = []
          scm_cmd(cmd_args) do |io|
            io.each_line do |line|
              diff << line
            end
          end
          logger.debug { "plastic diff: #{diff}" }
          diff
        rescue ScmCommandAborted
          nil
        end

        # not yet support (needs workspace or cm command )
        def cat(path, identifier=nil)
          logger.debug {"plastic cat path:#{path} id:#{identifier} " }
          return nil
        end        

        private

			  # Returns the relative url of the repository
			  # Eg: root_url = file:///var/svn/foo
			  #     url      = file:///var/svn/foo/bar
			  #     => returns /bar
			  def relative_url
			    @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(@root_url)}", Regexp::IGNORECASE), '')
			  end
			  
        def scm_cmd(args, options = {}, &block)
          cd_path_to_workspace = %w|cd|
          cd_path_to_workspace << "/D" if Redmine::Platform.mswin?
          cd_path_to_workspace << url
          cd_path_to_workspace << "&&"
          full_args = []
          full_args += args
          ret = shellout(
          				 cd_path_to_workspace.map { |e| e.to_s }.join(' ') + ' ' +
                   self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          if $? && $?.exitstatus != 0
            raise ScmCommandAborted, "plastic exited with non-zero status: #{$?.exitstatus}"
          end
          ret
        end
        
        def scm_cmd_no_rais(args, options = {}, &block)
          full_args = []
          full_args += args
          ret = shellout(
                   self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          ret
        end

        # Helper that iterates over the child elements of a xml node
        # MiniXml returns a hash when a single child is found
        # or an array of hashes for multiple children
        def each_xml_element(node, name)
          if node && node[name]
            if node[name].is_a?(Hash)
              yield node[name]
            else
              node[name].each do |element|
                yield element
              end
            end
          end
        end
        
      end
    end
  end
end
